# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import cPickle
import os
import re
import sys

import svn.fs
import svn.repos

import gvn.project
import gvn.userdb
import gvn.util


SVN_USERDB_PATH = "conf/gvn/userdb"


class HookInfo(object):
  # XXX everything references hi.pool, but this doesn't have it?!  How
  # the hell have the tests been passing?  Anyway, the hooks mustn't
  # be sharing one giant pool anyway.  runner.py needs to pass an
  # iterpool to each hook.
  pool = None

  # Subversion sends different values as different argument numbers
  # depending on the hook that's called, the variables below map
  # positional arguments in argv based on argv[0].
  #
  # For more definitions on the hooks and their arguments, see:
  #   http://svnbook.red-bean.com/nightly/en/svn-book.html#svn.ref.reposhooks

  ARGV_INDEX_MAP = {
    'start-commit': ['repos_path', 'user', 'capabilities'],
    'pre-commit':   ['repos_path', 'txn_name'],
    'post-commit':  ['repos_path', 'revision'],
    'pre-revprop-change':
                    ['repos_path', 'revision', 'user', 'propname', 'action'],
    'post-revprop-change':
                    ['repos_path', 'revision', 'user', 'propname', 'action'],
    'pre-lock':     ['repos_path', 'path', 'user'],
    'post-lock':    ['repos_path', 'user'],
    'pre-unlock':   ['repos_path', 'path', 'user'],
    'post-unlock':  ['repos_path', 'user'],
    }

  STDIN_MAP = {
    'pre-revprop-change':  'propvalue',
    'post-revprop-change': 'propvalue',
    'post-lock':   'locked_paths',
    'post-unlock': 'locked_paths',
  }

  def __init__(self, argv, fh=None, userdb=None):
    self._fh =             fh
    self._input =          None

    ## common to all hook scripts
    self._argv =           argv
    self._hook_name =      os.path.basename(argv[0])

    self._locked_paths = None

    # common svn objects used by a few methods
    self._repo =       None
    self._fs =         None

    # svn computed data for some hooks (populated on demand)
    self._head = self._head_root = None
    self._txn =            None
    self._txn_root =       None
    self._paths_changed =  None
    self._prefix_changed = None
    self._super_users =    None
    self._action_word =    None
    self._txn_props =      None
    self._author = self._date = self._log = None
    self._project_config = None

    # The easy one: repository path is always the first arg
    self.repos_path = argv[1]
    self.hook_dir = os.path.join(self.repos_path, 'hooks',
                                 self._hook_name + '.d')

    self._userdb = userdb

  def _GetArgvValue(self, name):
    try:
      idx = self.ARGV_INDEX_MAP[self._hook_name].index(name)
      return self._argv[idx+1]
    except (KeyError, ValueError):
      pass

    return None

  def _GetInputValue(self, name):
    try:
      if self.STDIN_MAP[self._hook_name] == name:
        if self._input is None and self._fh is not None:
          self._input = self._fh.read()
        return self._input
    except (KeyError, ValueError):
      pass

    return None

  # XXX Conflates hook failure with successful answer to the yes/no
  # authz question.
  def BreakOnHookFailure(self):
    """Check what the per-hook failure disposition is.

    Generally if a single hook fails then the code immediately stops
    running hooks.  However in certain situations even if a single hook
    fails the script should continue running all hooks.
    """
    if self._hook_name and self._hook_name.startswith('post-'):
      return False
    return True

  def PostProcessHookReturnCode(self, rcode):
    """Allow for last minute massaging of the return code.

    Generally this just passes the return code through.  However in certain
    situations the code pretends that everything succeeded regardless.
    """
    if self._hook_name and self._hook_name.startswith('post-'):
      return 0
    return rcode

  def repos_name(self):
    """Convenience method to return the 'short name' of a repository.

    This basically assumes that all the repositories of interest are in
    the same directory and the last component of the repository path is
    unique enough to be considered the 'short name'.
    """
    return os.path.basename(self.repos_path)

  def logger_name(self, script=None):
    """Returns a name suitable for use with logging.getLogger().
    """
    if not self.repos_name():
      return None

    name = ['svn']
    name.append(self.repos_name())
    name.append('hook')
    name.append(self.hook_name)
    if script is not None:
      name.append(script)
    name = [word.replace('.', '_') for word in name]
    try:
      name = '.'.join(name)
    except TypeError, e:
      # probably there's a None in there somewhere
      name = None
    return name


  arguments = property(lambda self: self._argv[1:])

  hook_name = property(lambda self: self._hook_name)

  action = property(lambda self: self._GetArgvValue('action'))

  path = property(lambda self: self._GetArgvValue('path'))

  propname = property(lambda self: self._GetArgvValue('propname'))

  revision = property(lambda self: int(self._GetArgvValue('revision')))

  txn_name = property(lambda self: self._GetArgvValue('txn_name'))

  user = property(lambda self: self._GetArgvValue('user'))

  propvalue = property(lambda self: self._GetInputValue('propvalue'))

  def _GetLockedPaths(self):
    if self._locked_paths is None:
      self._locked_paths = self._GetInputValue('locked_paths')
      if self._locked_paths is not None:
        self._locked_paths = self._locked_paths.split('\n')
    return self._locked_paths
  locked_paths = property(_GetLockedPaths)

  def _GetCapabilities(self):
    if self._capabilities is None:
      self._capabilities = set(self._GetArgvValue('action').split(':'))
    return self._capabilities
  capabilities = property(_GetCapabilities,
doc="""set of client capabilities (e.g. svn.ra.CAPABILITY_MERGEINFO)""")

  # We allocate everything in the global pool; seems unlikely we'll
  # need to get more complicated.

  def _GetRepo(self):
    if self._repo is None:
      self._repo = svn.repos.open(self.repos_path)
    return self._repo
  repo = property(_GetRepo)

  def _GetFs(self):
    if self._fs is None:
      self._fs = svn.repos.fs(self.repo)
    return self._fs
  fs = property(_GetFs)

  def _GetHead(self):
    if self._head is None:
      self._head = svn.fs.youngest_rev(self.fs)
    return self._head
  head = property(_GetHead)

  def _GetHeadRoot(self):
    if self._head_root is None:
      self._head_root = svn.fs.revision_root(self.fs, self.head)
    return self._head_root
  head_root = property(_GetHeadRoot)

  def _GetTxn(self):
    if self._txn is None:
      self._txn = svn.fs.open_txn(self.fs, self.txn_name)
    return self._txn
  txn = property(_GetTxn)

  def _GetTxnRoot(self):
    if self._txn_root is None:
      self._txn_root = svn.fs.txn_root(self.txn)
    return self._txn_root
  txn_root = property(_GetTxnRoot)

  def _GetPathsChanged(self):
    if self._paths_changed is None:
      self._paths_changed = svn.fs.paths_changed(self.txn_root)
    return self._paths_changed
  paths_changed = property(_GetPathsChanged)

  def _GetPrefixChanged(self):
    if self._prefix_changed is None:
      tmp = gvn.util.CommonPrefix(list(self.paths_changed.iterkeys()))
      # paths_changed have a leading /
      self._prefix_changed = tmp.lstrip('/')
    return self._prefix_changed
  prefix_changed = property(_GetPrefixChanged)

  # We store our list of svn superusers as a property attached to the repo root
  def _GetSuperUsers(self, index):
    if self._super_users is None:
      contents = svn.fs.node_prop(self.head_root, "", "gvn:superusers")
      if contents is None:
        self._super_users = (set(), set())
      else:
        self._super_users = gvn.util.ParseOwners(contents)
    return self._super_users[index]
  super_users = property(lambda self: self._GetSuperUsers(0))
  super_groups = property(lambda self: self._GetSuperUsers(1))

  def _Action_Word(self):
    if self._action_word is None:
      mappings = {
        "A": "add",
        "M": "modify",
        "D": "delete",
      }
      self._action_word = mappings[self.action]
      return self._action_word
  action_word = property(_Action_Word)

  def _GetUserdb(self):
    if self._userdb is None:
      self._userdb = gvn.userdb.UserDB(os.path.join(self.repos_path,
                                                    SVN_USERDB_PATH))
    return self._userdb
  userdb = property(_GetUserdb)

  # TODO(epg): Various hooks compute these locally; make them use this instead.
  def _GetTxnProps(self):
    if self._txn_props is None:
      self._txn_props = svn.fs.txn_proplist(self.txn, self.pool)
    return self._txn_props
  txn_props = property(_GetTxnProps)

  # TODO(epg): Various hooks compute these (or at least author)
  # locally; make them use these instead.
  def _GetSvnProp(self, name):
    attr = '_' + name
    val = getattr(self, attr)
    if val is not None:
      return val
    val = svn.fs.txn_prop(self.txn, 'svn:' + name, self.pool)
    setattr(self, attr, val)
    return val
  author = property(lambda self: self._GetSvnProp('author'))
  date = property(lambda self: self._GetSvnProp('date'))
  log = property(lambda self: self._GetSvnProp('log'))

  def _GetProjectConfig(self):
    # TODO(epg): Much of this is duplicated from gvn.project, because it's
    # heavily ra-based.  And, I think I've fixed some bugs here not fixed
    # for gvn.project's version of this :(.  Need to abstract this out.
    if self._project_config is None:
      # Start with defaults.
      config = dict(gvn.project._DEFAULT_CONFIG)
      # Try to find gvn:project property on self.prefix_changed or any of
      # its parents.
      prop = [None]
      def find_prop(path):
        try:
          prop[0] = svn.fs.node_prop(self.head_root, path, 'gvn:project',
                                     self.pool)
        except svn.core.SubversionException, e:
          if e.apr_err != svn.core.SVN_ERR_FS_NOT_FOUND:
            raise
          # Keep looking.
          return -1
        if prop[0] is None:
          # Keep looking.
          return -1
        # Got it.
        return 0
      path = gvn.util.ClimbAndFind(self.prefix_changed, find_prop)
      if path == '/':
        path = ''
      if prop[0] is not None:
        # Parse the project config dict from the property and process it.
        config.update(cPickle.loads(prop[0]))
      # Make change-branch-base relative to repository root.
      cbb = config['change-branch-base']
      if cbb.startswith('/'):
        config['change-branch-base'] = cbb.lstrip('/')
      else:
        if path == '':
          config['change-branch-base'] = cbb
        else:
          config['change-branch-base'] = '/'.join([path, cbb])
      config['path'] = path
      self._project_config = config
    return self._project_config
  project_config = property(_GetProjectConfig)
